﻿#region [ License information          ]
/* ************************************************************
 *
 * Copyright (c) Bjørn Bouet Smith, 2010, 2011
 *
 * This source code is subject to terms and conditions of 
 * Microsoft Public License (Ms-PL).
 * 
 * A copy of the license can be found in the license.txt
 * file at the root of this distribution. If you can not 
 * locate the License, please send an email to bjornsmith@gmail.com
 * 
 * By using this source code in any fashion, you are 
 * agreeing to be bound by the terms of the Microsoft 
 * Permissive License.
 *
 * You must not remove this notice, or any other, from this
 * software.
 *
 * ************************************************************/
#endregion

using System;
using System.IO;
using System.Text;
using Smith.AMC.Serialization;

namespace Smith.AMC.Operations
{
    /// <summary>
    /// Class that decodes server responses and return real objects or errors as needed.
    /// </summary>
    public class OperationResponseDecoder
    {
        /// <summary>
        /// Flag to indicate that a string should be deserialized.
        /// </summary>
        internal const ushort StringFlag = (ushort)TypeCode.String;

        /// <summary>
        /// The ItemSerializer that will be used to deserialize the data from the server.
        /// </summary>
        private static readonly ItemSerializer ItemSerializer = new ItemSerializer();

        /// <summary>
        /// Decodes the specified server response and returns the data blob as the specified type
        /// </summary>
        /// <typeparam name="T">The type to return the response as</typeparam>
        /// <param name="serverResponse">The server response.</param>
        /// <returns>The item if it is not null; null otherwise</returns>
        public static T DecodeResponse<T>(OperationResponse<T> serverResponse)
        {
            if (IsEmpty(serverResponse))
            {
                return default(T);
            }

            return Decode(serverResponse);
        }

        /// <summary>
        /// Determines whether the specified server response is a success.
        /// </summary>
        /// <param name="response">The response.</param>
        /// <returns>
        /// <c>true</c> if the specified response is a success; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsSuccess(OperationResponse response)
        {
            switch (response.OperationName)
            {
                case OperationNames.Get:
                case OperationNames.MultiGet:
                    EnrichOperationResponse(response);
                    return response.DataLength > 0;
                
                case OperationNames.CheckAndSet:
                case OperationNames.Add:
                case OperationNames.Replace:
                case OperationNames.Append:
                case OperationNames.Prepend:
                case OperationNames.Set:
                    // Expect STORED\r\n    
                    string storeResp = DecodeStringResponse(response);
                    return storeResp.Equals("STORED\r\n", StringComparison.Ordinal);

                case OperationNames.Delete:
                    // Expect DELETED\r\n
                    string deleteResp = DecodeStringResponse(response);
                    return deleteResp.Equals("DELETED\r\n", StringComparison.Ordinal);
                
                case OperationNames.FlushAll:
                    // Expect OK\r\n
                    string flushResp = DecodeStringResponse(response);
                    return flushResp.Equals("OK\r\n", StringComparison.Ordinal);
                
                case OperationNames.Stats:
                    // Expect something that starts with STAT
                    string statResp = DecodeStringResponse(response);
                    return statResp.StartsWith("STAT", StringComparison.Ordinal);
                
                default:
                    throw new InvalidOperationException(string.Format("Unknown command: {0} found", response.OperationName));
            }
        }

        /// <summary>
        /// Decodes the server response as a simple ulong response, for 
        /// server responses to the command : incr and decr
        /// </summary>
        /// <param name="serverResponse">The server response.</param>
        /// <returns>The value from the server or 0 (zero) if the value could not be parsed</returns>
        public static ulong DecodeUlongResponse(OperationResponse<ulong> serverResponse)
        {
            // serverresponse should be a stringrepresentation of the value as a byte array+\r\n
            // So lets subtract two characters from the end and to a Convert
            ulong response;
            string ulongasString = Encoding.UTF8.GetString(serverResponse.ServerResponse, 0, serverResponse.ServerResponse.Length - 2);
            if (ulong.TryParse(ulongasString, out response))
            {
                return response;
            }

            return 0;
        }

        /// <summary>
        /// Decodes the server response as a string
        /// </summary>
        /// <param name="response">The response top decode.</param>
        /// <returns>The server response as a string</returns>
        private static string DecodeStringResponse(OperationResponse response)
        {
            return (string)ItemSerializer.Deserialize(StringFlag, response.ServerResponse, 0, response.ServerResponse.Length);
        }

        /// <summary>
        /// Decodes the specified server response and returns the data blob as the specified type
        /// </summary>
        /// <typeparam name="T">The type to return the response as</typeparam>
        /// <param name="serverResponse">The server response.</param>
        /// <returns>The item if it is not null; null otherwise</returns>
        private static T Decode<T>(OperationResponse<T> serverResponse)
        {
            if (serverResponse.OperationName == OperationNames.Increment || serverResponse.OperationName == OperationNames.Decrement)
            {
                throw new InvalidOperationException("Cannot use Decode<T> with simple commands, use DecodeUlongResponse instead");
            }

            EnrichOperationResponse(serverResponse);

            return (T)ItemSerializer.Deserialize(serverResponse.Flags, serverResponse.ServerResponse, serverResponse.DataStart, serverResponse.DataLength);
        }

        /// <summary>
        /// Enriches the operation response by parsing the first line of the
        /// server response into the operation response object's properties
        /// </summary>
        /// <param name="response">The response.</param>
        private static void EnrichOperationResponse(OperationResponse response)
        {
            if (response.OperationName == "incr" || response.OperationName == "decr")
            {
                return;
            }

            if (response.DataStart > 0)
            {
                // response has already been enriched
                return;
            }

            using (MemoryStream ms = new MemoryStream(response.ServerResponse))
            {
                using (StreamReader reader = new StreamReader(ms))
                {
                    string responseHeader = reader.ReadLine();
                    string[] parts = responseHeader.Split(new[] { ' ' });
                    response.CacheKey = parts[1];
                    response.Flags = ushort.Parse(parts[2]);
                    response.DataLength = int.Parse(parts[3]);
                    response.DataStart = responseHeader.Length + 2;
                    if (parts.Length == 5)
                    {
                        // CAs is available
                        response.CASValue = ulong.Parse(parts[4]);
                    }
                }
            }
        }

        /// <summary>
        /// Determines whether the specified response is empty.
        /// </summary>
        /// <param name="response">The response.</param>
        /// <returns>
        /// <c>true</c> if the specified response is empty; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsEmpty(OperationResponse response)
        {
            if (response.ServerResponse.Length != 5)
            {
                return false;
            }

            if ((char)response.ServerResponse[0] == 'E' &&
                (char)response.ServerResponse[1] == 'N' &&
                (char)response.ServerResponse[2] == 'D' &&
                (char)response.ServerResponse[3] == '\r' &&
                (char)response.ServerResponse[4] == '\n')
            {
                return true;
            }

            return false;
        }
    }
}
